Skip to content

fix(plugin-grid,plugin-detail): draw the shared EmptyValue for missing cell values - #8503

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8491-handrolled-empty-placeholders
Sep 8, 2026
Merged

fix(plugin-grid,plugin-detail): draw the shared EmptyValue for missing cell values#8503
os-justin merged 2 commits into
mainfrom
claude/issue-8491-handrolled-empty-placeholders

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #8491
Fixes #8475

ObjectGrid and RelatedList each spelled their own empty-cell placeholder.
Both now draw the shared EmptyValue from @object-ui/components. Both cards
close here, as dispatched.

Reachability and the visual delta — the two things the brief asked me to measure

No new dependency edge, either side. plugin-grid already lists
@object-ui/components as a dependency and ObjectGrid.tsx already imported
EmptyValue (its no-renderer default branch has always used it).
plugin-detail already lists it as a peerDependency + devDependency and
RelatedList.tsx already imported from the same module; EmptyValue joins that
existing import list. Nothing was added to any manifest.

The visual change is DELIBERATE at four sites and a NO-OP at one. The three
grid cell sites and the related-list site drop text-xs italic and take the
shared component's typography. That is the point rather than a side effect:
ObjectGrid's no-renderer default branch already returned EmptyValue, so one
table could show a 12px italic placeholder in one column and the shared upright
one in the next. AGREEMENT (grid) and THE AGREEMENT (related list) pin that
the two branches now render byte-identical classNames.

The fifth site — the record-detail drawer — keeps its rendered text and its
text-sm italic typography through glyph and className, so its delta is
purely additive. Reason below.

The card counted three sites. There are four, and the class is NOT retired

The dispatch's stated justification was that a repo-wide grep found exactly two
carriers, so the class could be closed completely. That premise does not
survive a census by mechanism
, and the census is the part of this PR worth
reading. Every instrument below was run on the literal base sha
61e649ab1e7697bf54e2d651c383d31f282678f4, over packages/, apps/ and
examples/, excluding tests and generated files.

# instrument files sites verdict
1 the card's own exact class string text-muted-foreground/50 text-xs italic 2 4 reproduces the card exactly
2 any text-muted-foreground/50, order-independent 3 5 +1 site in ObjectGrid.tsx itself
3 class-agnostic: a span (or createElement('span', …)) whose only child is a bare em-dash 11 13 premise refuted
4 a bare em-dash as a ?: / ?? fallback, no span at all 33 41 premise refuted decisively
5 the literal string No value surfaces two more hand-rolled placeholders

Instrument 1 returned exactly what the card predicted because it grepped for
the string the card had already found.
That is the failure mode the brief
warned about, and re-running with a different instrument was the whole value of
the exercise.

What the wider instruments found:

  • Instrument 2 — the fourth site, in the file this card is about.
    ObjectGrid.tsx's record-detail drawer carried the same hand-rolled
    placeholder spelled text-sm instead of text-xs, holding t('grid.empty')
    instead of an em-dash. Same class, same missing aria-label, same missing
    data-slot. It is fixed here — it is the card's own defect in the card's
    own file, miscounted rather than out of scope.
  • Instrument 3 — nine more files. ResourceListPage.tsx's defaultCell,
    RecordDetailDrawer.tsx, AuditPanel.tsx, ImportWizard.tsx,
    PublicFormsPage.tsx, AiPendingActionsInbox.tsx all draw a hand-rolled
    em-dash for an empty cell/field value with no accessible name. Plus
    DetailSection.tsx and HeaderHighlight.tsx, which are hand-rolled but DO
    carry an aria-label (objectui#8475's own body names DetailSection, so the
    "only two carriers" table contradicted the sibling card it cited).
  • Instrument 4 — four MORE sites inside ObjectGrid.tsx, in the mobile card
    layout, as bare '—' strings with no span and therefore not even a muted
    class. These are NOT fixed here: converting a bare string fallback nested
    inside a styled span is a different shape from the one this card pinned, on a
    surface the card never mentions, and it needs its own validation. Reported for
    filing instead.

This PR reduces the class from 13 carriers to 8. It does not retire it.
The PM's conclusion (fold #8475 in, close both here) still holds — the two cards
name the two worst carriers, the only ones where the same screen shows an
announced and an unannounced placeholder side by side. Only the "retire the
class" reason was wrong.

Two more measured corrections

The mobile card layout is not a second read path for these sites. The brief
required pinning it because PR #8495 measured it as one. It calls the very same
col.cell(val, row) renderers — but its secondary-field loop drops empty values
before the call (hide empty values on mobile), so an empty field is omitted
from the card entirely, label and all, and no placeholder of either spelling has
ever reached it. Measured by rendering, not read from source. It IS a real
second read path for the populated half, which is pinned.

objectui#8475 mis-attributed its own evidence. Its body says
DateCellRenderer returns EmptyValue for an unparseable date, quoting if (date === null || isNaN(date.getTime())). That line is DateTimeCellRenderer's.
DateCellRenderer's only EmptyValue branch is if (!value), which
RelatedList's isValueEmpty has already intercepted — so an unparseable
date renders a formatted span and never reaches the shared component. The
card's conclusion (both placeholders coexist in one column) is real and is
pinned; it needs datetime, not date. The card itself flagged this claim as
"read from source, not driven end-to-end — worth one measurement before acting".

Why the drawer keeps its glyph

grid.empty has exactly one call site in the workspace, and
packages/i18n/src/__tests__/dead-key-batch-retired-4730.test.ts names that
call site as its evidence the key is live across all ten locale packs. Swapping
the drawer to a bare em-dash would strand a translated string in ten packs while
every gate stayed green — that file only checks pack presence, and the reverse
i18n sweep is report-only. So the drawer takes EmptyValue with
glyph={t('grid.empty')} and className="text-sm italic": same text, same
typography, plus data-slot / aria-label / select-none /
no-underline / pointer-events-none.

THE DRAWER — the rendered TEXT is unchanged is the only case that holds that
line, and ablation E proves it can fail.

Which cases discriminate — measured, not asserted

The caricature was run, not reasoned about: EmptyValue returned
unconditionally at all five sites, filled cells included. Of the 12 grid cases,
8 red and 4 GREEN. The four an implementation that has given up on values
entirely still passes, labelled as such in the file rather than quoted as proof:

  • THE DEFECT — an empty LINK+ACTION cell
  • THE DEFECT — the auto-linked PRIMARY cell
  • AGREEMENT — the linked branch and the no-renderer default branch
  • MOBILE CARD VIEW — the card layout OMITS an empty field

The first two are the most quotable assertions in the pin and they cannot tell
the fix from its worst caricature. What refuses it — by asserting both that the
value is present AND that no placeholder shares its cell — is the five
NON-REGRESSION / THE DRAWER — a FILLED field cases. THE DEFECT — an empty LINK cell and both empty-drawer cases also redden, but through their
value-bearing CONTROLS rather than their headline assertion; without those
controls they would have joined the green four.

Every DOM lookup is scoped to one row, one card or one dialog. PR #8495 measured
why: a grid pin asserting "no childless flex-wrap anywhere in the grid" failed
against the correct implementation, because ObjectGrid's toolbar renders a
legitimately empty one.

Ablations — each proved on disk, each restored by state

Every leg ran from the committed implementation under trap … EXIT INT TERM
with absolute paths; each mutation proved by git hash-object differing from
git rev-parse HEAD:PATH plus removed/injected grep counts in both
directions with the matched line printed; each restore verified by state
(git diff HEAD empty AND hashes equal), never by an exit code.

ObjectGrid.tsx at HEAD: 31e93c11b393529fcd45e8d3d76aadab6afcb585.
RelatedList.tsx at HEAD: e08d9c6da660feb8ede751b416b058a9f0630cfb.

leg mutation mutated blob red, by name
A the three hand-rolled cell spans, verbatim 0b1fc75ad61c2cf940cc586a46cfc467ebaab3e0 4/12 — the three THE DEFECT cell cases + AGREEMENT
B EmptyValue everywhere, filled cells included grid 9c6f315e89f9a5e135530c4de7232f560791ea98, list 43f69440b7bcf3dc6d40d28bc685a744eb3d5b88 8/12 grid + 3/3 list
C the drawer's hand-rolled span, verbatim d0c8569e3bf33eb5e6066f7180f3f1bc43626ef8 2/12 — both THE DRAWER placeholder cases
D RelatedList's hand-rolled span, verbatim 9bd5c5c192c8e04564c25935efa3e500de603c40 2/3 — THE DEFECT + THE AGREEMENT
E the drawer adopts the BARE shared component 9b734190d7f024a00e4e62fd32bf09baf716a8e5 1/12 — THE DRAWER — the rendered TEXT is unchanged

Changeset

minor for both packages. node scripts/check-changeset-presence.mjs verdict:

✅ 2 source file(s) of 2 released package(s) changed, and this change declares
1 changeset(s): .changeset/8491-8475-shared-empty-value-placeholder.md.

minor rather than patch because rendered output changes in two published
packages, and it matches the level the immediate sibling in this family
(objectui#8459, RelatedList's emptiness agreement) shipped at. major is
forbidden by the repo. skip-changeset is a phantom label here and was not
used.

Findings for filing (MCP search_issues was rate-limited)

search_issues returned API rate limit already exceeded for user ID 323634890, and REST is 403 from this container
(GitHub access is not enabled for this session). Per the dispatch contract I
did not file unsearched and did not drop them — the PM files these:

  1. Eight more carriers of this exact class, none with an accessible name:
    app-shell/.../ResourceListPage.tsx (defaultCell),
    plugin-dashboard/src/RecordDetailDrawer.tsx,
    app-shell/.../AuditPanel.tsx, plugin-grid/src/ImportWizard.tsx,
    apps/console/.../PublicFormsPage.tsx,
    plugin-chatbot/src/AiPendingActionsInbox.tsx, plus
    plugin-detail/src/DetailSection.tsx and
    plugin-detail/src/HeaderHighlight.tsx (hand-rolled but these two DO carry
    an aria-label, so they are the milder half).
  2. Four bare '—' fallbacks inside ObjectGrid.tsx's own mobile card
    layout
    — no span, no muted class, no accessible name. Same file as this
    card, different shape and different surface.
  3. The wider ?: / ?? em-dash population: 41 sites across 33 files.
    Worth a triage pass deciding which are empty-value placeholders (where
    EmptyValue applies) and which are legitimately something else.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

os-justin and others added 2 commits September 8, 2026 02:57
…g cell values

ObjectGrid and RelatedList each spelled their own empty-cell placeholder — a
span classed `text-muted-foreground/50 text-xs italic` holding a bare em-dash.
The shared `EmptyValue` carries a `data-slot`, an i18n-resolved `aria-label`
and `select-none` / `no-underline` / `pointer-events-none`; the hand-rolled
spans carried none of them, so an empty cell had no accessible name while its
renderer-supplied neighbour did, and inside a link column it looked clickable.

Four sites in ObjectGrid, not the three the originating census counted: the
record-detail drawer carried the same placeholder in a `text-sm` spelling. The
three cell sites adopt the bare shared component, which is a deliberate visual
change — they now match the `EmptyValue` the no-renderer default branch already
drew one column over. The drawer keeps its rendered text and typography through
`glyph` and `className`, because `grid.empty` has exactly one call site in the
workspace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ot the predicted one

The caricature (EmptyValue returned unconditionally, filled cells included) was
run rather than reasoned about. It reddens 8 of the 12 grid cases, not the
"every THE DEFECT case stays green" the docblock had predicted — three of them
redden through their value-bearing controls rather than their headline
assertion, and exactly four stay green. Both docblocks now name those four as
scope declarations and say which assertion refuses the caricature.

Two further corrections written into the files:

- The mobile card layout is NOT a second read path for these cell sites. It
  calls the same col.cell renderers, but drops empty values before the call, so
  an empty field is omitted from the card entirely and no placeholder has ever
  reached it. Measured by rendering.
- objectui#8475 attributed the reachable EmptyValue branch to DateCellRenderer;
  that line is DateTimeCellRenderer's. The card's conclusion holds, so the
  agreement case uses datetime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3473.6 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-Diru8tMD.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.87KB 114.10KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 191.36KB 53.16KB
fields (index.js) 243.24KB 61.42KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.73KB 63.97KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.37KB 56.63KB
plugin-kanban (index.js) 55.40KB 15.71KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin marked this pull request as ready for review September 8, 2026 03:45
@os-justin
os-justin added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 40a7c53 Sep 8, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-8491-handrolled-empty-placeholders branch September 8, 2026 04:01
os-justin pushed a commit that referenced this pull request Sep 8, 2026
…g raise could not (objectui#8542)

`fa9e76ccd` raised `PER_CHUNK_GZIP_CEILINGS.framework` to 100,000 and recorded,
honestly, that it could not say what the bytes bought: "the commits in that
window have never been bisected — this checker is a two-build predicate over
them and no one has run it." That is the one sentence this change replaces.

The bisect has now been run — one `apps/console` build per point, each from the
repo ROOT, `framework` read out of the `apps/console/dist/eager-closure.json`
the build itself writes:

  | build       | landed by | `framework` gzip | moved by                 |
  | `40a7c538a` | #8503     | 70,999           | last GREEN, 1 byte under |
  | `512c84b16` | #8519     | 70,999           |                        0 |
  | `f76f43628` | #8512     | 71,261           |                     +262 |
  | `e76634cc8` | #8529     | 72,245           |                     +984 |
  | `e411c3e58` | #8562     | 72,248           |                       +3 |

Two commits own the overage, not one, and the LARGER one is outside the window
every card bounded its reasoning to: `e76634cc8` landed after the red had
already started and carries 984 of the 1,246 bytes the pair added. A repair
scoped to that window would have left `main` red. `512c84b16` emitted a
byte-identical chunk to `40a7c538a` — the same `framework-nDOJv2Ij.js` content
hash — so the three commits between them moved this chunk by zero, and
`270f2825b` touches no file under `packages/(core|react|types)` at all.

What the bytes buy is therefore sayable, which is what the gate's own failure
message asks of a raise: both contributors are silent-wrong-answer fixes on one
file's filter path. `f76f43628` stopped an unrecognised `$` operator adding no
constraint and matching every row; `e76634cc8` stopped an array comparand and a
`{ $field }` reference being compared by reference, which selected every row
under `$ne` and no rows under `$eq` without saying so.

No constant moves. The prose that records what the raise COSTS is kept intact,
including that the attribution arrived after the ceiling did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment